<?php
/*
 用PHP实现的服务器，依赖http_parse扩展和swoole扩展
 直接做为裸露服务器可能会出现某些无法预料的问题
 可以在前面加一级nginx，以实现类似于nginx+php-fpm的功能
 性能上比nginx+php-fpm强大一些。
*/
 namespace Lib;
 use \Lib\Parser;
 use \Lib\EchoLog;
class HttpServer{
    protected $buffer_header = [];
    protected $requests = [];
    protected $buffers = [],$nparsed=[];
    protected $parser;

    public $config;
    protected $_onRequest;

    protected $currentFd;
    protected $headerInfo = [];
    /**
     * @var \swoole_server
     */
    public $serv;
    protected $charset = 'utf-8';

    protected $loger;
    public $pid_file;

    protected $cookies = [];

    protected $runtime = [];

    public $plugins = [];

    static $HttpStatus = array(
        100 => "100 Continue",
        101 => "101 Switching Protocols",
        200 => "200 OK",
        201 => "201 Created",
        204 => "204 No Content",
        206 => "206 Partial Content",
        300 => "300 Multiple Choices",
        301 => "301 Moved Permanently",
        302 => "302 Found",
        303 => "303 See Other",
        304 => "304 Not Modified",
        307 => "307 Temporary Redirect",
        400 => "400 Bad Request",
        401 => "401 Unauthorized",
        403 => "403 Forbidden",
        404 => "404 Not Found",
        405 => "405 Method Not Allowed",
        406 => "406 Not Acceptable",
        408 => "408 Request Timeout",
        410 => "410 Gone",
        413 => "413 Request Entity Too Large",
        414 => "414 Request URI Too Long",
        415 => "415 Unsupported Media Type",
        416 => "416 Requested Range Not Satisfiable",
        417 => "417 Expectation Failed",
        500 => "500 Internal Server Error",
        501 => "501 Method Not Implemented",
        503 => "503 Service Unavailable",
        506 => "506 Variant Also Negotiates"
    );
    const DATE_FORMAT_HTTP = 'D, d-M-Y H:i:s T';

    const HTTP_EOF = "\r\n\r\n";
    const HTTP_HEAD_MAXLEN = 2048; //http头最大长度不得超过2k

    const ST_FINISH = 1; //完成，进入处理流程
    const ST_WAIT   = 2; //等待数据
    const ST_ERROR  = 3; //错误，丢弃此包

    public function __construct($config){
        $this->pid_file = dirname(__DIR__).'/logs/'.APP_NAME.'.pid';
        $this->config = $config;
        $this->loger = new EchoLog('');
        register_shutdown_function(array($this, 'handleFatal'));
    }

    function run(){
        $swcfg = array_merge(
            [
                'log_file' => dirname(__DIR__).'/logs/'.APP_NAME.'.log',
                'worker_num' => 8,
                'max_request' => 1000,
                'max_conn' => 10000,
                'daemonize' => 0,
            ],$this->c('global'));
        $server = new \swoole_server($this->c('server.host'), $this->c('server.port'));
        $this->serv = $server;
        $server->set($swcfg);
        $server->on('Start',array($this,'onStart'));
        $server->on('ManagerStart', array($this,'onManagerStart'));
        $server->on('WorkerStart',array($this,'onWorkerStart'));
        $server->on('Connect', array($this, 'onConnect'));
        $server->on('Receive', array($this, 'onReceive'));
        $server->on('Close', array($this, 'onClose'));
        $server->on('Shutdown', array($this, 'onShutdown'));
        $server->on('Task', array($this, 'onTask'));
        $server->on('Finish', array($this, 'onFinish'));
        $server->start();
    }

    function onMasterStart($serv){
        $this->log(SERVER_NAME."[#master] start");
    }

    function onManagerStart($serv) {
        $this->setProcessName('php-manager '.APP_NAME.' ('.__DIR__.'/etc/'.APP_NAME.'.ini)');
        $this->log(SERVER_NAME."[#manager] start");
    }

    function onShutdown($serv){
        is_file($this->pid_file) && unlink($this->pid_file);
        exec('rm -rf '.SHM_PATH);
        $this->log(SERVER_NAME." shutdown");
        apply_action('on_shutdown',$this,$serv);
    }

    function onStart($serv){        
        $this->loadPlugin();
        $this->setProcessName('php-master ' . APP_NAME . ' host=' . $this->config['server']['host'] . ' port=' . $this->config['server']['port']);
        $this->c('global.daemonize') && file_put_contents($this->pid_file, $serv->master_pid);
        apply_action('server_start',$serv);
    }

    function onWorkerStart($server,$worker_id){
        ($user = $this->c('server.user')) && $this->changeUser($user);
        if ($worker_id >= $this->c('global.worker_num')){
            $this->setProcessName('php-task '.APP_NAME);
            $this->log("php-task[#{$worker_id}] running on ".$this->c('server.host').":".$this->c('server.port'));
        }else{
            $this->setProcessName('php-worker '.APP_NAME);
            $this->log("php-worker[#{$worker_id}] running on ".$this->c('server.host').":".$this->c('server.port'));
        }
        $this->loadPlugin();
        apply_action('on_worker_start',$server);
    }

    /**
    * 异步任务
    * @access public
    * @param
    * @return
    */
    function onTask($serv, $task_id, $from_id, $data){
        try{
            $arr = explode('|',$data,2);
            $task_name = $arr[0];
            $params = '';
            if(isset($arr[1])){
                $params = $arr[1];
            }
            $task = '\\Task\\'.ucwords($task_name);
            $task::run($params);
        }catch(\Exception $e){
            $this->log($e->getMessage());
        }
        // $serv->finish();
    }

    function onFinish($serv, $task_id, $data){
        echo 'task data:'.$data;
    }

    function onConnect($serv, $client_id, $from_id){
        apply_action('on_connect',$this,$serv,$client_id,$from_id);
    }

    /**
     * @param \swoole_server $serv
     * @param $fd
     * @param $from_id
     * @param $data
     */
    function onReceive($serv, $fd, $from_id, $data){
    
        if(empty($this->buffers[$fd]) || empty($this->parser)){
            $this->parser = new Parser;
        }
        $this->buffers[$fd] .= $data;
        $buffer = &$this->buffers[$fd];
        $this->parser->execute($buffer);
        $info = $serv->connection_info($fd);
        
        if ($this->parser->hasError())
        {
            $serv->send($this->parser->getError());
            $serv->close($fd);
        }

        if ($this->parser->isFinished()){
            if(defined('DEBUG')) {
                $this->log("new request:\"".$_SERVER['HTTP_METHOD'].' '.$_SERVER['REQUEST_URI']."\" fd={$fd} from={$from_id}");
            }
            $_SERVER['CXSV_INFO'] = $info;
            $_SERVER['REMOTE_ADDR'] = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : $info['remote_ip'];
            $this->currentFd = $fd;
            $this->clear($fd);
            call_user_func($this->_onRequest, $serv);
        }
        
    }

    function onRequest($callback){
        $this->_onRequest = $callback;
    }

    /**
     * @param \swoole_server $serv
     * @param $fd
     * @param $from_id
     */
    function onClose($serv, $fd, $from_id){
        $this->clear($fd);
        if(defined('DEBUG')) $this->log("client[#$fd@$from_id] close");
    }

    public function setProcessName($name){
        swoole_set_process_name($name);
    }

    /**
    * 获得配置项
    * @access public
    * @param string $key
    * @return string
    */
    public function c($key){
        $key = explode('.',$key);
        $val = isset($this->config[$key[0]]) ? $this->config[$key[0]] : '';
        isset($key[1]) && !empty($val) && isset($val[$key[1]]) && $val = $val[$key[1]];
        return $val;
    }

    protected function clear($fd){
        $this->buffers[$fd] = "";
        $this->nparsed[$fd] = 0;
        $this->cookies[$fd] = [];
        $this->headerInfo = [];
        $this->parser = null;
    }

    /**
     * 改变进程的用户ID
     * @param $user
     */
    public function changeUser($user){
        if (!function_exists('posix_getpwnam')){
            trigger_error(__METHOD__.": require posix extension.");
            return;
        }
        $user = posix_getpwnam($user);
        if($user){
            posix_setuid($user['uid']);
            posix_setgid($user['gid']);
        }
    }

    private function loadPlugin(){
        $paths = array('global'=>FRAME_ROOT.'plugin/','app'=>$this->document_root.'Plugin/');
        foreach($paths as $type => $plugin_path){
            $odir = opendir($plugin_path);
            while($file = readdir($odir)){
                if($file{0}!='.' && ($path=$plugin_path.$file.'/') && ($plugin_file=$path.$file.'.php') && is_file($plugin_file) && (is_file($path.'enabled') || $type == 'global')){
                    include $plugin_file;
                }
            }
        }
    }

    function http404(){
        return $this->error('404 Not Found','你所请求的页面已去了火星，不再回来了~~',404);
    }

    function http302($url){
        $response = array(
            'HTTP/1.1 302',
        );
        $response[] = 'Location:'.$url;
        $response[] = 'Content-Length:0';
        if(isset($this->cookies[$this->currentFd])){
            foreach($this->cookies[$this->currentFd] as $cookie){
                $response[]= "Set-Cookie:".$cookie;
            }
        }
        $response[] = '';
        $response[] = '';
        $this->serv->send($this->currentFd, join("\r\n",$response));
        //添加这一句，是为了终止程序继续执行
        throw new \Exception("", 1);
    }

    function header($key, $value){
        $this->headerInfo[$key] = $value;
    }

    function setCookie($name,$value,$expires=0,$path='/',$domain=''){
        $this->cookies[$this->currentFd][] = "{$name}=".trim($value).";".($expires ? "expires=".gmdate("D, d-M-Y H:i:s",time()+$expires)." GTM;" : "")."path={$path};domain={$domain}";
    }

    function response($respData, $code = 200){
        $response = array(
            'HTTP/1.1 '.$code.' '.self::$HttpStatus[$code],
        );
        
        $headers = array(
            'Cache-Control'=>'must-revalidate,no-cache',
            'Content-Language'=>'zh-CN',
            'Server'=>SERVER_NAME,
            'Content-Type'=>'text/html;charset=utf-8',
            'Content-Length'=>strlen($respData),
        );
        $headers  = array_merge($headers,$this->headerInfo);
        foreach($headers as $key=>$val){
            $response[] = $key.':'.$val;
        }
        if(isset($this->cookies[$this->currentFd]) && !empty($this->cookies[$this->currentFd])){
            foreach($this->cookies[$this->currentFd] as $cookie){
                $response[]= "Set-Cookie:".$cookie;
            }
        }
        $response[] = '';
        $response[] = $respData;
        $this->serv->send($this->currentFd, join("\r\n",$response));
        // $this->serv->close($this->currentFd);
    }

    static function parseCookie($strHeaders){
        $result = array();
        $aHeaders = explode(';', $strHeaders);
        foreach ($aHeaders as $line)
        {
            list($k, $v) = explode('=', trim($line), 2);
            $result[$k] = urldecode($v);
        }
        return $result;
    }

    public function log($msg){
        $this->loger->info($msg);
    }

    public function error($msg,$content,$code = 200){
        $this->response(\Lib\Error::info($msg,$content),$code);
    }

    /**
     * Fatal Error的捕获
     * @codeCoverageIgnore
     */
    public function handleFatal(){
        $error = error_get_last();
        if (!isset($error['type'])){
            $data = ob_get_contents();
            ob_end_clean();
            $data .= ' <font color="red">worker restarted</font>';
            $this->response($data);
            return;
        } 
        if(ob_get_contents())
            ob_end_clean();
        $error['file'] = explode('.',$error['file'])[0];
        $errorMsg = "{$error['message']} (".str_replace([SHM_PATH.'tpls/',SHM_PATH.'widget/tpl/',APP_PATH,FRAME_ROOT],['tpl:','widget_tpl:','app:','root:'],$error['file']).":{$error['line']})";
        $message = \Lib\Error::info("Server fatal error", $errorMsg);
        $this->response($message);
    }
}
